; minihttpd, tiny barebones clojure web server
; http://alan.xen.prgmr.com/
 
(ns com.marzhillstudios.molehill.http-server
  (:use [clojure.contrib.duck-streams :only (reader writer read-lines spit to-byte-array)]
        [clojure.contrib.str-utils :only (re-split str-join re-gsub)])
  (:import (java.net ServerSocket URLDecoder)
           (java.io File)
           (java.util.concurrent Executors)))
 
(def codes {200 "HTTP/1.0 200 OK" 
            404 "HTTP/1.0 404 Not Found"
            403 "HTTP/1.0 403 Forbidden"})
 
(def mimes {"html" "Content-type: text/html" 
            "css"  "Content-type: text/css" 
            "png"  "Content-type: image/png" 
            "js"  "Content-type: text/javascript" 
            "txt"  "Content-type: text/plain"})
 
(def error {403 "The first soft snow!\nEnough to bend the leaves\nYour access denied\n\n403"
            404 "Sick and feverish\nGlimpse of cherry blossoms\nThe file was not found\n\n404"})
 
(defn build-headers [& headers]
  "Build a header string and end it with two spaces"
  (str (str-join "\r\n" headers) "\r\n\r\n"))
 
(defn log [& info]
  (println (str-join "\t" info)))
 
(defn send-resource [sock file]
  "Open the socket's output stream and send headers and content"
  (with-open [os (.getOutputStream sock)]
    (let [path (.getAbsolutePath file)
          headers (build-headers 
                    (codes 200) 
                    (mimes (last (re-split #"\." path)) "txt")
                    (format "Content-length: %s" (.length file)))]
      (.write os (to-byte-array headers) 0 (count headers))
      (.write os (to-byte-array file) 0 (.length file))
      (log (java.util.Date.) (.getInetAddress sock) path))))
 
(defn send-error [sock code]
  "Open the socket's output stream and send header and error message"
  (with-open [wrt (writer (.getOutputStream sock))]
    (spit wrt (str (build-headers (codes code) (mimes "txt")) (error code)))))
 
(defn check-index [uri]
  "Check if we should serve out default index page"
  (if (.endsWith uri "/") (str uri "index.html") uri))
 
(defn handle-request [sock doc-root]
  "Send the file if it exists, or a 404"
  (with-open [rdr (reader (.getInputStream sock))]
    (let [uri (second (re-split #"\s+" (first (line-seq rdr))))
          file (File. (str doc-root (check-index (re-gsub #"\.{2,}" "" (URLDecoder/decode uri)))))]
      (log (java.util.Date.) (.toString (.getInetAddress sock))
           (format "Attempting serve: [%s]" (check-index (URLDecoder/decode uri))))
      (if (.exists file)
        (if (.isDirectory file)
          (send-error sock 403)
          (send-resource sock file))
        (send-error sock 404)))))
 
(defn http-listen [socket doc-root]
  "Bind to a socket and keep listening"
  (let [client-socket (.accept socket)]
    (try
      (handle-request client-socket doc-root)
      (catch Exception _ (log (java.util.Date.) (.toString (.getInetAddress client-socket)) "Peer disconnected"))))
  (recur socket doc-root))

(defn http-start
  [port doc-root]
    (http-listen (ServerSocket. port) doc-root))

